package com.ge.healthcare.autosc.common.util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import com.ge.healthcare.autosc.common.ASCLogger;

/**
 * Title: File Comparison Description: Compares two files and returns a
 * FileComparison result that includes information on whether the files are
 * equal or not, if not it includes the index of the byte where they first
 * differ and a "difference window" which is a buffer more or less centered on
 * the first difference as an aid for debugging.
 * 
 * Copyright: Copyright (c) 2000 Company: GEMS
 * 
 * @author MDK
 * @version 1.0
 */
public class FileComparison {

	/**
	 * Construct a file comparison for two file objects.
	 * 
	 * @param file1
	 *            the first file.
	 * @param file2
	 *            the second file.
	 */
	public FileComparison(File file1, File file2) {
		file1_ = file1;
		file2_ = file2;
	}

	/**
	 * Construct a file comparison for two files given their names.
	 * 
	 * @param file1
	 *            the first file name.
	 * @param file2
	 *            the second file name.
	 */
	public FileComparison(String fileName1, String fileName2) {
		file1_ = new File(fileName1);
		file2_ = new File(fileName2);
	}

	/**
	 * Compare two files assuming the content is binary.
	 * 
	 * @return the result of the comparison.
	 * @throws FileNotFoundException
	 *             if one or both of the files does not exist.
	 */
	public FileComparisonResult compareAsBinary() throws FileNotFoundException {
		if (result_ != null)
			return result_;

		checkFilesExist();

		byte[] buffer1 = new byte[BUFSIZE_];
		byte[] buffer2 = new byte[BUFSIZE_];

		long firstDiffIndex = compareFiles(buffer1, buffer2);
		long bufferDiffIndex;
		byte[] diffWindow1;
		byte[] diffWindow2;
		boolean isEqual;

		if (firstDiffIndex > -1) {
			isEqual = false;
			bufferDiffIndex = firstDiffIndex % BUFSIZE_;
			diffWindow1 = getBinaryWindow(buffer1, bufferDiffIndex);
			diffWindow2 = getBinaryWindow(buffer2, bufferDiffIndex);
		} else {
			isEqual = true;
			diffWindow1 = new byte[0];
			diffWindow2 = new byte[0];
		}

		result_ = new FileComparisonResult(isEqual, firstDiffIndex,
				diffWindow1, diffWindow2);
		return result_;
	}

	/**
	 * Compare two files assuming the content is text.
	 * 
	 * @return the result of the comparison.
	 * @throws FileNotFoundException
	 *             if one or both of the files does not exist.
	 */
	public FileComparisonResult compareAsText() throws FileNotFoundException {
		if (result_ != null)
			return result_;

		checkFilesExist();

		byte[] buffer1 = new byte[BUFSIZE_];
		byte[] buffer2 = new byte[BUFSIZE_];

		long firstDiffIndex = compareFiles(buffer1, buffer2);
		long bufferDiffIndex;
		String diffWindow1;
		String diffWindow2;
		boolean isEqual;

		if (firstDiffIndex > -1) {
			isEqual = false;
			bufferDiffIndex = firstDiffIndex % BUFSIZE_;
			diffWindow1 = new String(getBinaryWindow(buffer1, bufferDiffIndex));
			diffWindow2 = new String(getBinaryWindow(buffer2, bufferDiffIndex));
		} else {
			isEqual = true;
			diffWindow1 = "";
			diffWindow2 = "";
		}

		result_ = new FileComparisonResult(isEqual, firstDiffIndex,
				diffWindow1, diffWindow2);
		return result_;
	}

	/**
	 * Checks if the two files are equal.
	 * 
	 * @return true if equal, false if one or both do not exist.
	 */
	public boolean isEqual() {
		try {
			compareAsBinary();
			return result_.isEqual();
		} catch (FileNotFoundException fnfe) {
			return false;
		}
	}

	/**
	 * Check to see if the files exist.
	 * 
	 * @throws FileNotFoundException
	 *             if one or both files do not exist.
	 */
	private void checkFilesExist() throws FileNotFoundException {
		if (file1_.exists() == false)
			throw new FileNotFoundException(file1_.getAbsolutePath());
		if (file2_.exists() == false)
			throw new FileNotFoundException(file2_.getAbsolutePath());
	}

	/**
	 * Compare the two files using the supplied buffers.
	 * 
	 * @param buffer1
	 *            the buffer for file1.
	 * @param buffer2
	 *            the buffer for file2.
	 * @throws FileNotFoundException
	 *             if one or both files do not exist.
	 */
	private long compareFiles(byte[] buffer1, byte[] buffer2)
			throws FileNotFoundException {
		BufferedInputStream in1 = new BufferedInputStream(new FileInputStream(
				file1_));
		BufferedInputStream in2 = new BufferedInputStream(new FileInputStream(
				file2_));

		long firstDiffIndex = -1;
		long diffCount = 0;
		int bufferDiffIndex;

		try {
			int bytesRead1 = in1.read(buffer1);
			int bytesRead2 = in2.read(buffer2);

			while (moreBytes(bytesRead1, bytesRead2)) {
				bufferDiffIndex = compareBuffers(bytesRead1, buffer1,
						bytesRead2, buffer2);

				if (bufferDiffIndex > -1) {
					firstDiffIndex = diffCount + bufferDiffIndex;
					break;
				}

				diffCount += bytesRead1; // Sames as bytesRead2, otherwise
											// bufferDiffIndex != -1

				bytesRead1 = in1.read(buffer1);
				bytesRead2 = in2.read(buffer2);
			}
		} catch (IOException ioe) {
			ASCLogger.error(this.getClass(), "compareFiles",
					"Failed to compare buffers. Exception:" + ioe.getMessage(),
					ioe);
		} finally {
			try {
				in1.close();
				in2.close();
			} catch (IOException ioe) {
				ASCLogger.debug(this.getClass(), "compareFiles",
						"Failed to close input stream");
			}
		}
		return firstDiffIndex;
	}

	/**
	 * Determines if there are more bytes to read.
	 * 
	 * @param size1
	 *            the size returned from reading the first stream.
	 * @param size2
	 *            the size returned from reading the second stream.
	 */
	private boolean moreBytes(int size1, int size2) {
		if ((size1 > -1) || (size2 > -1))
			return true;
		return false;
	}

	/**
	 * Compares two buffers.
	 * 
	 * @param size1
	 *            the size of the first buffer (may be less then
	 *            buffer1.length).
	 * @param buffer1
	 *            the first buffer.
	 * @param size2
	 *            the size of the second buffer.
	 * @param buffer2
	 *            the second buffer.
	 * @return the index of the first difference between buffers, -1 if there is
	 *         none.
	 */
	private int compareBuffers(int size1, byte[] buffer1, int size2,
			byte[] buffer2) {
		int size = (size1 < size2) ? size1 : size2;

		for (int i = 0; i < size; i++) {
			if (buffer1[i] != buffer2[i])
				return i;
		}

		int maxSize = (size1 < size2) ? size2 : size1;
		if (size < maxSize)
			return size; // Occurs when one file is longer.

		return -1;
	}

	/**
	 * Return a subset of the buffer, more or less centered on the index.
	 * 
	 * @param buffer
	 *            the buffer to extract the window from.
	 * @param long the index into the buffer where the difference occurs.
	 * @return a buffer of bytes more or less centered around index.
	 */
	private byte[] getBinaryWindow(byte[] buffer, long index) {
		int myIndex;
		int begin;
		int end;

		if (index > Integer.MAX_VALUE) {
			ASCLogger.error(this.getClass(), "getBinaryWindow",
					"Buffer index exceeded maximum expected");
			Thread.dumpStack();
			myIndex = buffer.length;
		} else {
			myIndex = (int) index;
		}

		if (myIndex + (WINDOWSIZE_ / 2) > buffer.length) {
			begin = buffer.length - WINDOWSIZE_;
			end = buffer.length;
		} else {
			begin = myIndex - (WINDOWSIZE_ / 2);
			end = myIndex + (WINDOWSIZE_ / 2);

			if (begin < 0)
				begin = 0;
		}

		int len = end - begin;
		byte[] result = new byte[len];
		System.arraycopy(buffer, begin, result, 0, len);

		return result;
	}

	/** The first file of the comparison set. */
	private File file1_;

	/** The second file of the comparison set. */
	private File file2_;

	/** The result of comparing the two files, cached. */
	private FileComparisonResult result_ = null;

	/** The buffer size for the input buffers. */
	private static final int BUFSIZE_ = 1024;

	/** The nominal width of the difference window. */
	private static final int WINDOWSIZE_ = 80;
}
